/*
 * ENUM Discoverer. ENUM Demonstration software, demonstrating the potential of ENUM.
 * 
 * Copyright (C) 2009 SIDN and ISOC.nl
 * 
 * This file is part of ENUM Discoverer.
 *
 * ENUM Discoverer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * ENUM Discoverer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with ENUM Discoverer.  If not, see <http://www.gnu.org/licenses/>.
 */

package nl.sidn.enumdiscoverer.contacts;

import java.util.TreeMap;

import android.os.SystemClock;
import android.util.Log;

import nl.sidn.enumdiscoverer.LogConfig;

/**
 * Provides mutual exclusion on a particular contact ID.
 * The Contacts API prior to Android 2.0 doesn't expose transactions or batch
 * operations, so we need some way to prevent concurrent modifications to the same
 * contact. This doesn't protect us from some other application interfering, but
 * it's much better than nothing.
 * This is NOT a reentrant lock: if it has been acquired multiple times, a single call
 * to release() will make it available to other threads. It will log warnings (at DEBUG
 * level) if it is acquired/released multiple times.
 * @author	Grant Patterson (grant.patterson@catalyst.net.nz)
 */
public class ContactLock {

	static final String TAG = LogConfig.getLogTag(ContactLock.class);
	// whether DEBUG level logging is enabled (whether globally, or explicitly for this log tag)
	static final boolean DEBUG = LogConfig.isDebug(TAG);
	// whether VERBOSE level logging is enabled
	static final boolean VERBOSE = LogConfig.VERBOSE;
	
	// An application-wide ContactLock exposed through getApplicationWideLock()
	private static ContactLock sAppWideLock = new ContactLock();
	
	/**
	 * The contact IDs currently locked.
	 * All access to this should be synchronised
	 */
	private final TreeMap<Long, Thread> mAcquired = new TreeMap<Long, Thread>();
	
	private ContactLock() {}
	
	/**
	 * Acquire the lock on id for this thread, waiting indefinitely for the lock to become available.
	 * @param id The ID to acquire a lock for
	 */
	public void acquire(long id) {
		
		if (VERBOSE) Log.v(TAG, "Acquiring lock for id=" + id);
		
		synchronized (mAcquired) {
			Thread lockHolder = mAcquired.get(id);
			
			while (lockHolder != null) {
				
				if (lockHolder == Thread.currentThread()) {
					if (DEBUG) Log.d(TAG, "This thread (" + Thread.currentThread().getName() +
							") already holds the lock for id=" + id);
					// Don't wait (we would be waiting forever). We already have the lock - just return
					return;
				}
				// someone else has the lock for id
				
				try {
					mAcquired.wait();
				} catch (InterruptedException e) {
					if (DEBUG) Log.d(TAG, "Interruption swallowed");
				}
				
				lockHolder = mAcquired.get(id);
			}
			
			// have acquired the lock
			mAcquired.put(id, Thread.currentThread());
		}
	}
	
	/**
	 * Try to acquire a lock on id for this thread, waiting for up to timeout.
	 * @param id The ID to acquire a lock for.
	 * @param timeout The maximum time in milliseconds to wait for a lock on id
	 * @return whether a lock was acquired
	 */
	public boolean tryAcquire(long id, long timeout) {
		
		if (VERBOSE) Log.v(TAG, "Trying to acquire lock for id=" + id);
		
		synchronized (mAcquired) {
			
			long expiry = SystemClock.uptimeMillis() + timeout;
			
			Thread lockHolder = mAcquired.get(id);
			
			while (lockHolder != null) {
				
				if (SystemClock.currentThreadTimeMillis() > expiry) {
					if (VERBOSE) Log.v(TAG, "Timeout trying to acquire lock for id=" + id);
					return false;
				}
				
				if (lockHolder == Thread.currentThread()) {
					if (DEBUG) Log.d(TAG, "This thread (" + Thread.currentThread().getName() +
							") already holds the lock for id=" + id);
					// Don't wait (we would be waiting forever). We already have the lock - just return
					return true;
				}
				
				try {
					mAcquired.wait(timeout);
				} catch (InterruptedException e) {
					if (DEBUG) Log.d(TAG, "Interruption swallowed");
				}
				
				lockHolder = mAcquired.get(id);
			}
			
			// have acquired the lock
			mAcquired.put(id, Thread.currentThread());
			return true;
		}
	}
	
	/**
	 * Release this thread's lock on id
	 * @param id The ID to release the lock for
	 * @throws IllegalMonitorStateException if the lock has been acquired by another thread
	 */
	public void release(long id) {
		
		if (VERBOSE) Log.v(TAG, "Releasing lock for id=" + id);
		
		synchronized (mAcquired) {
			// release the id
			Thread lockHolder = mAcquired.remove(id);
			if (lockHolder == null) {
				// lock hasn't been acquired for id. Something's not right
				if (DEBUG) Log.d(TAG, "Can't release id " + id + ". Wasn't acquired");
				// notify anyway
			} else if (lockHolder != Thread.currentThread()) {
				// the lock isn't ours to release
				throw new IllegalMonitorStateException();
			}
			mAcquired.notifyAll();
		}
	}
	
	/**
	 * Gets the application-wide ContactLock
	 * @return An application-wide ContactLock
	 */
	public static ContactLock getApplicationWideLock() {
		return sAppWideLock;
	}
}
